/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import Logger, {LOG_MODULE_TYPE} from 'arkanalyzer/lib/utils/logger';
import {BaseChecker, BaseMetaData} from '../BaseChecker';
import {Rule} from '../../model/Rule';
import {Defects, IssueReport} from '../../model/Defects';
import {MatcherCallback} from '../../matcher/Matchers';
import path from 'path';
import fs from 'fs';
import {Json5parser} from '../../Index';
import {ArkAssignStmt, ArkFile, ArkMethod, ArkPtrInvokeExpr, Scene, Stmt} from 'arkanalyzer';
import {StringConstant} from 'arkanalyzer/lib/core/base/Constant';

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'RedundantDependencyCheck');
const gMetaData: BaseMetaData = {
    severity: 3,
    ruleDocPath: 'docs/redundant-dependency-check.md',
    description: ''
};

const DEPENDENCIES_WITH_BRACE_PATTERN = /^\s*["']?dependencies["']?\s*:\s*\{/;
const DEPENDENCIES_WITHOUT_BRACE_PATTERN = /^\s*["']?dependencies["']?\s*:/;
const OPEN_BRACE_PATTERN = /{/g;
const CLOSE_BRACE_PATTERN = /}/g;

export class RedundantDependencyCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public issues: IssueReport[] = [];

    public registerMatchers(): MatcherCallback[] {
        const matchFileCb: MatcherCallback = {
            matcher: undefined,
            callback: this.check
        };
        return [matchFileCb];
    }

    public check = (scene: Scene): void => {
        const files = scene.getFiles();
        if (files.length === 0) {
            return;
        }
        const {isModuleCheck, targetModulePath, projectPath} = this.analyzeScope(files);
        if (isModuleCheck && targetModulePath) {
            this.checkModuleOnly(targetModulePath, files);
        } else if (projectPath) {
            this.checkWholeProject(projectPath, files);
        }
    };

    private analyzeScope(files: ArkFile[]): {
        isModuleCheck: boolean;
        targetModulePath: string | null;
        projectPath: string | null
    } {
        if (files.length === 0) {
            return {isModuleCheck: false, targetModulePath: null, projectPath: null};
        }
        const firstFile = files[0];
        const projectPath = firstFile.getProjectDir();
        const modulePaths = new Set<string>();
        files.forEach(file => {
            const moduleScene = file.getModuleScene();
            const modulePath = moduleScene ? moduleScene.getModulePath() : projectPath;
            modulePaths.add(modulePath);
        });
        if (modulePaths.size === 1) {
            const modulePath = Array.from(modulePaths)[0];
            if (modulePath !== projectPath) {
                return {
                    isModuleCheck: true,
                    targetModulePath: modulePath,
                    projectPath
                };
            }
        }
        return {
            isModuleCheck: false,
            targetModulePath: null,
            projectPath
        };
    }

    private checkModuleOnly(modulePath: string, files: ArkFile[]): void {
        const modulePkg = this.parseOhPackage(path.join(modulePath, 'oh-package.json5'));
        const moduleDeps = new Set(this.extractDependencies(modulePkg));
        if (moduleDeps.size === 0) {
            return;
        }
        const allImports = new Set<string>();

        files.forEach(arkFile => {
            const fileModuleScene = arkFile.getModuleScene();
            const fileModulePath = fileModuleScene ? fileModuleScene.getModulePath() : arkFile.getProjectDir();
            if (fileModulePath === modulePath) {
                const imports = this.collectFileImports(arkFile);
                imports.forEach(imp => allImports.add(imp));
            }
        });
        this.processDependencyUsage(Array.from(allImports), moduleDeps);
        if (moduleDeps.size > 0) {
            this.reportRedundantDeps(modulePath, Array.from(moduleDeps), 'module');
        }
    }

    private checkWholeProject(projectPath: string, files: ArkFile[]): void {
        const projectPkg = this.parseOhPackage(path.join(projectPath, 'oh-package.json5'));
        const projectDeps = new Set(this.extractDependencies(projectPkg));
        const moduleDepsMap = new Map<string, Set<string>>(); // modulePath -> dependencies
        files.forEach(arkFile => {
            const moduleScene = arkFile.getModuleScene();
            const modulePath = moduleScene ? moduleScene.getModulePath() : projectPath;

            if (modulePath !== projectPath && !moduleDepsMap.has(modulePath)) {
                const modulePkg = this.parseOhPackage(path.join(modulePath, 'oh-package.json5'));
                const moduleDeps = this.extractDependencies(modulePkg);
                moduleDepsMap.set(modulePath, new Set(moduleDeps));
            }
        });
        files.forEach(arkFile => {
            const imports = this.collectFileImports(arkFile);
            const moduleScene = arkFile.getModuleScene();
            const modulePath = moduleScene ? moduleScene.getModulePath() : projectPath;
            this.processDependencyUsage(imports, projectDeps);
            if (modulePath !== projectPath) {
                const moduleDeps = moduleDepsMap.get(modulePath);
                if (moduleDeps) {
                    this.processDependencyUsage(imports, moduleDeps);
                }
            }
        });
        if (projectDeps.size > 0) {
            this.reportRedundantDeps(projectPath, Array.from(projectDeps), 'project');
        }
        moduleDepsMap.forEach((deps, modulePath) => {
            if (deps.size > 0) {
                this.reportRedundantDeps(modulePath, Array.from(deps), 'module');
            }
        });
    }

    private processDependencyUsage(imports: string[], dependencies: Set<string>): void {
        const depArray = Array.from(dependencies).sort((a, b) => b.length - a.length);
        imports.forEach(imp => {
            if (dependencies.has(imp)) {
                dependencies.delete(imp);
                return;
            }
            const matchedDep = depArray.find(dep => dependencies.has(dep) && imp.startsWith(dep + '/'));
            if (matchedDep) {
                dependencies.delete(matchedDep);
            }
        });
    }

    private collectFileImports(file: ArkFile): string[] {
        const imports: Set<string> = new Set();
        try {
            file.getImportInfos().forEach((importInfo) => {
                if (importInfo.getFrom() && !imports.has(<string>importInfo.getFrom())) {
                    imports.add(<string>importInfo.getFrom());
                }
            });
            const dynamicImports = new Set<string>();
            const methods: ArkMethod[] = [];
            const defaultMethod = file.getDefaultClass()?.getDefaultArkMethod();
            if (defaultMethod) {
                methods.push(defaultMethod);
            }
            file.getClasses().forEach(cls => {
                methods.push(...cls.getMethods());
            });
            methods.forEach(meth => {
                const stmts = meth.getCfg()?.getStmts();
                if (stmts) {
                    this.processStatementsForImports(stmts, dynamicImports);
                }
            });
            dynamicImports.forEach(imp => {
                if (!imports.has(imp)) {
                    imports.add(imp);
                }
            });
        } catch (err) {
            logger.error(`Failed to collect imports from ${file.getFilePath()}: ${(err as Error).message}`);
        }
        return Array.from(imports);
    }

    private processStatementsForImports(stmts: Stmt[], imports: Set<string>): void {
        stmts.forEach(stmt => {
            if (!(stmt instanceof ArkAssignStmt)) {
                return;
            }
            const rightOp = stmt.getRightOp();
            if (!(rightOp instanceof ArkPtrInvokeExpr)) {
                return;
            }
            if (rightOp.getMethodSignature().getMethodSubSignature().getMethodName() === 'import') {
                const args = rightOp.getArgs()[0];
                if (args instanceof StringConstant) {
                    imports.add(args.getValue());
                }
            }
        });
    }

    private parseOhPackage(packagePath: string): Record<string, unknown> {
        if (!fs.existsSync(packagePath)) {
            return {};
        }
        try {
            const text = fs.readFileSync(packagePath, 'utf-8');
            return Json5parser.parseJsonText(text);
        } catch (err) {
            logger.error(`Failed to parse ${packagePath}: ${(err as Error).message}`);
            return {};
        }
    }

    private extractDependencies(data: Record<string, unknown>): string[] {
        const deps: string[] = [];
        const depSection = data.dependencies;
        if (depSection && typeof depSection === 'object') {
            for (const key of Object.keys(depSection as Record<string, unknown>)) {
                deps.push(key);
            }
        }
        return deps;
    }

    private reportRedundantDeps(basePath: string, redundantDeps: string[], level: 'project' | 'module'): void {
        const pkgPath = path.join(basePath, 'oh-package.json5');
        const severity = this.rule.alert ?? this.metaData.severity;
        redundantDeps.forEach(dep => {
            const warnInfo = this.getDependencyWarnInfo(pkgPath, dep);
            const message = `Unused dependency '${dep}' found at ${level} level`;
            const defects = new Defects(warnInfo.line, warnInfo.startCol, warnInfo.endCol, message, severity, this.rule.ruleId,
                pkgPath, this.metaData.ruleDocPath, true, false, false);
            this.issues.push(new IssueReport(defects, undefined));
        });
    }

    private getDependencyWarnInfo(packagePath: string, dependency: string): WarnInfo {
        if (!fs.existsSync(packagePath)) {
            return {line: 1, startCol: 1, endCol: 1};
        }
        try {
            const readData = fs.readFileSync(packagePath, 'utf8');
            const readLines: string[] = readData.split('\n');
            let inDependencies = false;
            let braceCount = 0;
            let dependenciesStartLine = 0;
            for (let lineNumber = 0; lineNumber < readLines.length; lineNumber++) {
                const line = readLines[lineNumber];
                if (!inDependencies && DEPENDENCIES_WITH_BRACE_PATTERN.test(line)) {
                    inDependencies = true;
                    dependenciesStartLine = lineNumber + 1;
                    braceCount = 1;
                    const closeBraceIndex = line.indexOf('}', line.indexOf('{') + 1);
                    if (closeBraceIndex !== -1) {
                        braceCount = 0;
                        inDependencies = false;
                    }
                    const depPattern = new RegExp(`["']?${dependency}["']?\\s*:`, 'g');
                    let match;
                    if (braceCount === 0) {
                        continue;
                    }
                    while ((match = depPattern.exec(line)) !== null) {
                        const startCol = match.index + 1;
                        const endCol = startCol + dependency.length;
                        return {
                            line: lineNumber + 1,
                            startCol,
                            endCol
                        };
                    }
                } else if (!inDependencies && DEPENDENCIES_WITHOUT_BRACE_PATTERN.test(line)) {
                    inDependencies = true;
                    dependenciesStartLine = lineNumber + 1;
                    braceCount = 0;
                }
                if (inDependencies) {
                    braceCount += (line.match(OPEN_BRACE_PATTERN) || []).length;
                    const depPattern = new RegExp(`["']?${dependency}["']?\\s*:`, 'g');
                    let match;
                    while ((match = depPattern.exec(line)) !== null) {
                        const startCol = match.index + 1;
                        const endCol = startCol + dependency.length;
                        return {
                            line: lineNumber + 1,
                            startCol,
                            endCol
                        };
                    }
                    braceCount -= (line.match(CLOSE_BRACE_PATTERN) || []).length;
                    if (braceCount <= 0) {
                        inDependencies = false;
                    }
                }
            }
            return {
                line: dependenciesStartLine > 0 ? dependenciesStartLine : 1,
                startCol: 1,
                endCol: 1
            };
        } catch (error) {
            logger.error(`Failed to get dependency warn info for ${packagePath}: ${(error as Error).message}`);
            return {line: 1, startCol: 1, endCol: 1};
        }
    }
}

interface WarnInfo {
    line: number;
    startCol: number;
    endCol: number;
}